/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Javadoc.java
 * Copyright (C) 2006-2012,2015 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * Abstract superclass for classes that generate Javadoc comments and replace
 * the content between certain comment tags.
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public abstract class Javadoc implements OptionHandler {

    /** the start tag */
    protected String[] m_StartTag = null;

    /** the end tag */
    protected String[] m_EndTag = null;

    /** the classname */
    protected String m_Classname = Javadoc.class.getName();

    /** whether to include the stars in the Javadoc */
    protected boolean m_UseStars = true;

    /** the directory above the class to update */
    protected String m_Dir = "";

    /** whether to suppress error messages (no printout in the console) */
    protected boolean m_Silent = false;

    /**
     * Returns an enumeration describing the available options.
     * 
     * @return an enumeration of all the available options.
     */
    @Override
    public Enumeration<Option> listOptions() {
        Vector<Option> result = new Vector<Option>();

        result.addElement(new Option("\tThe class to load.", "W", 1, "-W <classname>"));

        result.addElement(new Option("\tSuppresses the '*' in the Javadoc.", "nostars", 0, "-nostars"));

        result.addElement(new Option("\tThe directory above the package hierarchy of the class.", "dir", 1, "-dir <dir>"));

        result.addElement(new Option("\tSuppresses printing in the console.", "silent", 0, "-silent"));

        return result.elements();
    }

    /**
     * Parses a given list of options.
     * 
     * @param options the list of options as an array of strings
     * @throws Exception if an option is not supported
     */
    @Override
    public void setOptions(String[] options) throws Exception {
        String tmpStr;

        tmpStr = Utils.getOption('W', options);
        if (tmpStr.length() > 0) {
            setClassname(tmpStr);
        } else {
            setClassname(this.getClass().getName());
        }

        setUseStars(!Utils.getFlag("nostars", options));

        setDir(Utils.getOption("dir", options));

        setSilent(Utils.getFlag("silent", options));
    }

    /**
     * Gets the current settings of this object.
     * 
     * @return an array of strings suitable for passing to setOptions
     */
    @Override
    public String[] getOptions() {
        Vector<String> result;

        result = new Vector<String>();

        result.add("-W");
        result.add(getClassname());

        if (!getUseStars()) {
            result.add("-nostars");
        }

        if (getDir().length() != 0) {
            result.add("-dir");
            result.add(getDir());
        }

        if (getSilent()) {
            result.add("-silent");
        }

        return result.toArray(new String[result.size()]);
    }

    /**
     * sets the classname of the class to generate the Javadoc for
     * 
     * @param value the new classname
     */
    public void setClassname(String value) {
        m_Classname = value;
    }

    /**
     * returns the current classname
     * 
     * @return the current classname
     */
    public String getClassname() {
        return m_Classname;
    }

    /**
     * sets whether to prefix the Javadoc with "*"
     * 
     * @param value true if stars are used
     */
    public void setUseStars(boolean value) {
        m_UseStars = value;
    }

    /**
     * whether the Javadoc is prefixed with "*"
     * 
     * @return whether stars are used
     */
    public boolean getUseStars() {
        return m_UseStars;
    }

    /**
     * sets the dir containing the file that is to be updated. It is the dir above
     * the package hierarchy of the class.
     * 
     * @param value the directory containing the classes
     */
    public void setDir(String value) {
        m_Dir = value;
    }

    /**
     * returns the current dir containing the class to update. It is the dir above
     * the package name of the class.
     * 
     * @return the current directory
     */
    public String getDir() {
        return m_Dir;
    }

    /**
     * sets whether to suppress output in the console
     * 
     * @param value true if output is to be suppressed
     */
    public void setSilent(boolean value) {
        m_Silent = value;
    }

    /**
     * whether output in the console is suppressed
     * 
     * @return true if output is suppressed
     */
    public boolean getSilent() {
        return m_Silent;
    }

    /**
     * prints the given object to System.err
     * 
     * @param o the object to print
     */
    protected void println(Object o) {
        if (!getSilent()) {
            System.err.println(o.toString());
        }
    }

    /**
     * returns true if the class can be instantiated, i.e., has a default
     * constructor.
     * 
     * @return true if the class can be instantiated
     */
    protected boolean canInstantiateClass() {
        boolean result;
        Class<?> cls;

        result = true;
        cls = null;

        try {
            cls = Class.forName(getClassname());
        } catch (Exception e) {
            result = false;
            println("Cannot instantiate '" + getClassname() + "'! Class in CLASSPATH?");
        }

        if (result) {
            try {
                cls.newInstance();
            } catch (Exception e) {
                result = false;
                println("Cannot instantiate '" + getClassname() + "'! Missing default constructor?");
            }
        }

        return result;
    }

    /**
     * Returns a new instance of the class
     * 
     * @return a new instance of the class
     */
    protected Object getInstance() {
        Object result;
        Class<?> cls;

        result = null;

        try {
            cls = Class.forName(getClassname());
            result = cls.newInstance();
        } catch (Exception e) {
            result = null;
        }

        return result;
    }

    /**
     * converts the given String into HTML, i.e., replacing some char entities with
     * HTML entities.
     * 
     * @param s the string to convert
     * @return the HTML conform string
     */
    protected String toHTML(String s) {
        String result;

        result = s;

        result = result.replaceAll("&", "&amp;");
        result = result.replaceAll("<", "&lt;");
        result = result.replaceAll(">", "&gt;");
        result = result.replaceAll("@", "&#64;");
        result = result.replaceAll("\n", "<br>\n");

        return result;
    }

    /**
     * indents the given string by a given number of indention strings
     * 
     * @param content   the string to indent
     * @param count     the number of times to indent one line
     * @param indentStr the indention string
     * @return the indented content
     */
    protected String indent(String content, int count, String indentStr) {
        String result;
        StringTokenizer tok;
        int i;

        tok = new StringTokenizer(content, "\n", true);
        result = "";
        while (tok.hasMoreTokens()) {
            if (result.endsWith("\n") || (result.length() == 0)) {
                for (i = 0; i < count; i++) {
                    result += indentStr;
                }
            }
            result += tok.nextToken();
        }

        return result;
    }

    /**
     * generates and returns the Javadoc for the specified start/end tag pair.
     * 
     * @param index the index in the start/end tag array
     * @return the generated Javadoc
     * @throws Exception in case the generation fails
     */
    protected abstract String generateJavadoc(int index) throws Exception;

    /**
     * generates and returns the Javadoc
     * 
     * @return the generated Javadoc
     * @throws Exception in case the generation fails
     */
    protected String generateJavadoc() throws Exception {
        String result;
        int i;

        result = "";

        for (i = 0; i < m_StartTag.length; i++) {
            if (i > 0) {
                result += "\n\n";
            }
            result += generateJavadoc(i).trim();
        }

        return result;
    }

    /**
     * determines the base string of the given indention string, whether it's either
     * only spaces (one space will be retured) or mixed mode (tabs and spaces, in
     * that case the same string will be returned)
     * 
     * @param str the string to analyze
     * @return the indention string
     */
    protected String getIndentionString(String str) {
        String result;

        // only spaces?
        if (str.replaceAll(" ", "").length() == 0) {
            result = " ";
        } else if (str.replaceAll("\t", "").length() == 0) {
            result = "\t";
        } else {
            result = str;
        }

        return result;
    }

    /**
     * determines the number of indention strings that have to be inserted to
     * generated the given indention string.
     * 
     * @param str the string to analyze
     * @return the number of base indention strings to insert
     */
    protected int getIndentionLength(String str) {
        int result;

        // only spaces?
        if (str.replaceAll(" ", "").length() == 0) {
            result = str.length();
        } else if (str.replaceAll("\t", "").length() == 0) {
            result = str.length();
        } else {
            result = 1;
        }

        return result;
    }

    /**
     * generates and returns the Javadoc for the specified start/end tag pair
     * 
     * @param content the current source code
     * @param index   the index in the start/end tag array
     * @return the generated Javadoc
     * @throws Exception in case the generation fails
     */
    protected String updateJavadoc(String content, int index) throws Exception {
        StringBuffer resultBuf;
        int indentionLen;
        String indentionStr;
        String part;
        String tmpStr;

        // start and end tag?
        if ((content.indexOf(m_StartTag[index]) == -1) || (content.indexOf(m_EndTag[index]) == -1)) {
            println("No start and/or end tags found: " + m_StartTag[index] + "/" + m_EndTag[index]);
            return content;
        }

        // replace option-tags
        resultBuf = new StringBuffer();
        while (content.length() > 0) {
            if (content.indexOf(m_StartTag[index]) > -1) {
                part = content.substring(0, content.indexOf(m_StartTag[index]));
                // is it a Java constant? -> skip
                if (part.endsWith("\"")) {
                    resultBuf.append(part);
                    resultBuf.append(m_StartTag[index]);
                    content = content.substring(part.length() + m_StartTag[index].length());
                } else {
                    tmpStr = part.substring(part.lastIndexOf("\n") + 1);
                    indentionLen = getIndentionLength(tmpStr);
                    indentionStr = getIndentionString(tmpStr);
                    part = part.substring(0, part.lastIndexOf("\n") + 1);
                    resultBuf.append(part);
                    resultBuf.append(indent(m_StartTag[index], indentionLen, indentionStr) + "\n");
                    resultBuf.append(indent(generateJavadoc(index), indentionLen, indentionStr));
                    resultBuf.append(indent(m_EndTag[index], indentionLen, indentionStr));
                    content = content.substring(content.indexOf(m_EndTag[index]));
                    content = content.substring(m_EndTag[index].length());
                }
            } else {
                resultBuf.append(content);
                content = "";
            }
        }

        return resultBuf.toString().trim();
    }

    /**
     * updates the Javadoc in the given source code.
     * 
     * @param content the source code
     * @return the updated source code
     * @throws Exception in case the generation fails
     */
    protected String updateJavadoc(String content) throws Exception {
        String result;
        int i;

        result = content;

        for (i = 0; i < m_StartTag.length; i++) {
            result = updateJavadoc(result, i);
        }

        return result;
    }

    /**
     * generates the Javadoc and returns it applied to the source file if one was
     * provided, otherwise an empty string.
     * 
     * @return the generated Javadoc
     * @throws Exception in case the generation fails
     */
    public String updateJavadoc() throws Exception {
        StringBuffer contentBuf;
        BufferedReader reader;
        String line;
        String result;
        File file;

        result = "";

        // non-existing?
        file = new File(getDir() + "/" + getClassname().replaceAll("\\.", "/") + ".java");
        if (!file.exists()) {
            println("File '" + file.getAbsolutePath() + "' doesn't exist!");
            return result;
        }

        try {
            // load file
            reader = new BufferedReader(new FileReader(file));
            contentBuf = new StringBuffer();
            while ((line = reader.readLine()) != null) {
                contentBuf.append(line + "\n");
            }
            reader.close();
            result = updateJavadoc(contentBuf.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result.trim();
    }

    /**
     * generates either the plain Javadoc (if no filename specified) or the updated
     * file (if a filename is specified). The start and end tag for the global info
     * have to be specified in the file in the latter case.
     * 
     * @return either the plain Javadoc or the modified file
     * @throws Exception in case the generation fails
     */
    public String generate() throws Exception {
        if (getDir().length() == 0) {
            return generateJavadoc();
        } else {
            return updateJavadoc();
        }
    }

    /**
     * generates a string to print as help on the console
     * 
     * @return the generated help
     */
    public String generateHelp() {
        String result;
        Enumeration<Option> enm;
        Option option;

        result = getClass().getName().replaceAll(".*\\.", "") + " Options:\n\n";
        enm = listOptions();
        while (enm.hasMoreElements()) {
            option = enm.nextElement();
            result += option.synopsis() + "\n" + option.description() + "\n";
        }

        return result;
    }

    /**
     * runs the javadoc producer with the given commandline options
     * 
     * @param javadoc the javadoc producer to execute
     * @param options the commandline options
     */
    protected static void runJavadoc(Javadoc javadoc, String[] options) {
        try {
            try {
                if (Utils.getFlag('h', options)) {
                    throw new Exception("Help requested");
                }

                javadoc.setOptions(options);
                Utils.checkForRemainingOptions(options);

                // directory is necessary!
                if (javadoc.getDir().length() == 0) {
                    throw new Exception("No directory provided!");
                }
            } catch (Exception ex) {
                String result = "\n" + ex.getMessage() + "\n\n" + javadoc.generateHelp();
                throw new Exception(result);
            }

            System.out.println(javadoc.generate() + "\n");
        } catch (Exception ex) {
            System.err.println(ex.getMessage());
        }
    }
}
